
import moo, { Rules, Token } from 'moo'
import * as AST from './shaderlib-ast'
import { formatError, ParsingError, ParsingErrorRecorder } from './shaderlib-err'

let _shaderParseCounter = 0

const mooRules: Rules = {
    ws: /[ \t]+/,
    newline: {match: '\n', lineBreaks: true},
    identifier:  /[a-zA-Z_][\-a-zA-Z0-9_]*/,
    number:  /[0-9]+/,
    string: {match: /"(?:\\["\\]|[^\n"\\])*"/, value: s => s.slice(1, -1)},

    lcurl: '{',
    rcurl: '}',
    lparen: '(',
    rparen: ')',
    lsqur: '[',
    rsqur: ']',

    colon: ':',
    semi: ';',
    comma: ',',
    dot: '.',
    at: '@',

    eq: '==',
    ueq: '!=',
    assign: '=',

    arrow: '->',
    lt: '<',
    gt: '>',

    add: '+',
    sub: '-',
    mul: '*',
    div: '/',
    comment: /\/\/.*?$/,

    macro: /\#[a-zA-Z]+/
}

const mooLexer = moo.compile(mooRules)


const EOF_TOK: Token = {
    value: '<EOF>',
    offset: 0,
    text: '<EOF>',
    lineBreaks: 0,
    line: 0,
    col: 0,
    type: 'EOF'
}

function _tokIs(tok: Token, t: string) {
    return tok.text === t
}

function _tokIn(tok: Token, ts: string[]) {
    return ts.some((v) => _tokIs(tok, v))
}

class ShaderLibParser {
    protected _errRecorder: ParsingErrorRecorder

    protected _source: string
    protected _tokStream: Array<Token>
    protected _tp = 0

    protected _filename: string

    constructor(source: string, filename: string, errRecorder: ParsingErrorRecorder) {
        this._errRecorder = errRecorder

        this._source = source
        this._filename = filename

        this._tokStream = Array.from(mooLexer.reset(source))
    }

    protected _nextTok(skipBlank: boolean = true): Token {
        let nt: Token
        do {
            if (this._tp + 1 >= this._tokStream.length) {
                EOF_TOK.line = this._tokStream[this._tp].line
                this._tp++
                return EOF_TOK
            }

            this._tp++
            nt = this._tokStream[this._tp]
        } while (skipBlank && (nt.type == 'newline' || nt.type == 'ws'))

        return nt
    }

    protected _nowTok(skipBlank: boolean = true): Token {
        do {
            if (this._tokStream[this._tp] === undefined)
                return EOF_TOK

            let nt = this._tokStream[this._tp]

            if (skipBlank && (nt.type === 'ws' || nt.type === 'newline')) {
                this._tp++
                continue
            }

            return nt
        } while(true)
    }

    protected _except(tok: string, throwError: boolean = true): boolean {
        const nt = this._nowTok()

        if (nt.text !== tok) {
            this._syntaxError(`except '${tok}', got '${nt.value}'`)
            return false
        }

        this._nextTok()

        return true
    }

    protected _nowTokIs(tok: string) {
        const nt = this._nowTok()
        if (nt.text !== tok) {
            return false
        }
        return true
    }

    protected _nowTokTypeIs(t: string) {
        return this._nowTok().type === t
    }

    protected _syntaxError(msg: string) {
        return this._errRecorder.recordSyntaxError(
            'Syntax Error',
            this._nowTok().line,
            msg,
            this._filename
        )
    }

    protected _parsePass(): AST.NodePassBody {
        if (!this._except('Pass')) return null
        if (!this._except('{')) return null

        const pass: AST.NodePassBody = {
            passName: '(default pass)',
            cullMode: 'back',
            zWrite: true,

            enableBlend: false,
            blendSrcFactor: 'one',
            blendDstFactor: 'zero',
            blendOp: 'add',

            wgslProgram: '',
            wgslReflect: null
        }

        do {
            const nt = this._nowTok()

            if (_tokIs(nt, 'Cull')) {
                const nt = this._nextTok()

                if (!_tokIn(nt, AST.gpuCullModeArray))
                    return this._syntaxError(`invaild cull mode: ${nt.text}, available options: ` + AST.gpuCullModeArray.join(' | '))

                pass.cullMode = <GPUCullMode> nt.value
                this._nextTok()

            } else if (_tokIs(nt, 'BlendOp')) {
                const nt = this._nextTok()

                if (!_tokIn(nt, AST.gpuBlendOp))
                    return this._syntaxError(`invaild blend op: ${nt.text}, available options: ` + AST.gpuBlendOp.join(' | '))

                pass.enableBlend = true
                pass.blendOp = <GPUBlendOperation> nt.value
                this._nextTok()

            } else if (_tokIs(nt, 'Blend')) {
                pass.enableBlend = true

                let nt = this._nextTok()

                if (!_tokIn(nt, AST.gpuBlendFactor))
                    return this._syntaxError(`invaild blend op: ${nt.text}, available options: ` + AST.gpuBlendFactor.join(' | '))

                pass.blendSrcFactor = <GPUBlendFactor> nt.value
                
                nt = this._nextTok()

                if (!_tokIn(nt, AST.gpuBlendFactor))
                    return this._syntaxError(`invaild blend op: ${nt.text}, available options: ` + AST.gpuBlendFactor.join(' | '))

                pass.blendDstFactor = <GPUBlendFactor> nt.value

                this._nextTok()

            } else if (_tokIs(nt, 'Name')) {
                const nt = this._nextTok()

                if (!this._nowTokTypeIs('string')) {
                    return this._syntaxError('required string')
                }

                pass.passName = nt.value
                this._nextTok()

            } else if (_tokIs(nt, 'ZWrite')) {
                const nt = this._nextTok()

                if (!_tokIn(nt, ['on', 'off']))
                    return this._syntaxError(`invaild z-write state: ${nt.text}, available options: ` + ['on', 'off'].join(' | '))

                pass.zWrite = nt.value === 'on'
                this._nextTok()

            } else if (this._nowTokIs('EOF')) {
                return this._syntaxError('no wgsl program provided')

            } else if (_tokIs(nt, 'WGSLPROGRAM')) {
                this._nextTok()
                break

            } else {
                return this._syntaxError('unexcepted token: ' + nt.text)
            }
        } while (true);

        // build wgsl code from token stream
        
        let wgslSource = ''
        let nt: Token

        do {
            nt = this._nowTok(false)

            if (nt.type === 'EOF') {
                return this._syntaxError('required \'WGSLEND\'')
            } else if (nt.text === 'ENGWGSL') {
                this._nextTok()
                break
            }

            wgslSource += nt.type === 'ws' ? ' ' : nt.text
            this._nextTok(false)
        } while (true)

        pass.wgslProgram = wgslSource

        if (!this._except('}')) return null

        return pass
    }

    protected _parseShaderBody(): AST.NodeShaderBody {
        if (!this._except('{')) return null

        const passes: Array<AST.NodePassBody> = []

        while (this._nowTokIs('Pass')) {
            const pass = this._parsePass()

            passes.push(pass)
        }

        return {
            passes: passes,
            tags: {}
        }
    }

    protected _parseShaderLib(): AST.NodeShaderLib {
        if (!this._except('Shader')) {
            return null
        }

        let shaderName = 'default_shader_' + _shaderParseCounter++

        if (this._nowTokTypeIs('string')) {
            shaderName = this._nowTok().value
            this._nextTok()
        }

        const body = this._parseShaderBody()

        return {
            shaderName: shaderName,
            shaderBody: body
        }
    }

    protected _parseShader() {
        const node = this._parseShaderLib()
        return node
    }

    protected _preprocess() {
        const defineSymbols: string[] = []
        const newTokStream: Token[] = []

        let nt = this._nowTok()

        while (nt.type !== 'EOF') {
            if (nt.type === 'macro') {
                if (nt.value.toLowerCase() === '#define') {
                    nt = this._nextTok()

                    if (nt.type !== 'identifier') {
                        return this._syntaxError('identifier required')
                    }

                    defineSymbols.push(nt.value)
                    this._nextTok()
                } else if (nt.value.toLowerCase() === '#ifdef' || nt.value.toLowerCase() === '#ifndef') {
                    const cmd = nt.value.toLowerCase()

                    nt = this._nextTok()

                    if (nt.type !== 'identifier') {
                        return this._syntaxError('identifier required')
                    }

                    const testSymbol = nt.value

                    nt = this._nextTok()
                    
                    let writeFlag = defineSymbols.indexOf(testSymbol) >= 0

                    writeFlag = cmd === '#ifdef' ? writeFlag : !writeFlag

                    while (true) {
                        if (nt.text === '#endif') {
                            this._nextTok(false)
                            break

                        } else if (nt.text === '#else') {
                            writeFlag = !writeFlag

                        } else if (writeFlag) {
                            newTokStream.push(nt)

                        } else if (nt.type === 'EOF') {
                            return this._syntaxError('#if wasnot closed')

                        }

                        nt = this._nextTok(false)
                    }
                } else {
                    return this._syntaxError('invaild macro: ' + nt.value)
                }
            } else {
                newTokStream.push(nt)
                this._nextTok(false)
            }

            nt = this._nowTok(false)
        }

        this._tokStream = newTokStream

        console.log(defineSymbols)

        return true
    }

    parse(): AST.NodeShaderLib {
        if (this._tokStream.length == 0)
            return null

        if (!this._preprocess()) {
            return null
        }

        this._tp = 0

        return this._parseShader()
    }
}

interface ParseResult {
    noError: boolean
    node: AST.NodeShaderLib
    errors: Array<ParsingError>
}

export function parseShaderLib(source: string, filename: string) {
    const recorder = new ParsingErrorRecorder()
    const parser = new ShaderLibParser(source, filename, recorder)

    const node = parser.parse()

    const errMsgs = recorder.getAllMessages()
    
    let errStr = ''

    if (errMsgs.length > 0) {
        errMsgs.forEach((err) => {
            errStr += formatError(err) + '\n'
        })
        throw new Error(errStr)
    } else {
        return node
    }
}

export function tryParseShaderLib(source: string, filename: string): ParseResult {
    const recorder = new ParsingErrorRecorder()
    const parser = new ShaderLibParser(source, filename, recorder)

    const node = parser.parse()

    const errMsgs = recorder.getAllMessages()

    return {
        noError: errMsgs.length == 0,
        node: node,
        errors: errMsgs
    }
}